很多時候我們會需要搜集些不同的資料。像是 Marketing 在做大規模但針對不同組織的調查問卷。如果只是三份、五份的問卷要做客製化、統整算還好;但如果是一百份、甚至上千份時,總不能一個個複製了吧。此時就會遇到個問題——
因為篇幅關係,這邊會拆成三篇來寫,第一篇與第二篇回應 Q1;第三篇回應 Q2。今天這篇是針對 Q1 的第二篇,昨天我們講了怎麼樣複製與簡單客製 Google 表單。而今天來到了第十二天,我們來到目前最進階的操作,用 GAS 完成超·客製表單。一樣先講結論,如果你很急著用,可以直接使用這份 Add-On: Form Publisher,功能非常強大。自己寫的好處是,如果你一天突然要做些高度客製化,那此篇會有幫助。這篇的定位比較像是字典、工具,在你需要用的時候可以來參照。那就讓我們開始吧!
複製表單有兩種方式。一種是比較簡單的「複製範本」,簡單來說就是針對一個表單複製,然後再改期中的元素。另一種是「從零製作」,這種就比較複雜,因為會是透過 GAS 完整製作表單,會需要比較熟悉 GAS。我們昨天講完方法一,今天則著重在方法二。
如果是比較複雜的表單與設定,舉例來說,有些人需要第一題,有些人第五題要用填空,那要怎麼處理?這個時候可以用 GAS 來製作每份表單。雖然這樣客製化程度很高,但相對製作時間會比前一次更久。果然是充滿取捨的人生。
也給大家看一下這次預計生成的參數們,左邊綠色是我們輸入的參數,右邊橘色是我們預計輸出的參數。
補充:校稿時有朋友問說那個「勾勾」怎麼做,多錄一支影片給大家看
設定步驟跟之前一一樣,從 Google Sheet 中進入 GAS。
昨天我們有示範如何操作既存的表單,今天我們來看看怎麼生出新的表單。方式極其簡單,就是用 FormApp.create()
,並在括號中輸入表單名稱即可。
function createNewForm(){
let new_form = FormApp.create('New Form');
}
但,這樣創造有個小問題,就是創造的位置會在根目錄,也就是一開始打開 Google Drive 的位置。那要怎麼移動?目前是需要比較陽春的透過寫 DriveApp
的 moveTo(folder)
才行。也就是以下示範的程式碼,改編自 JLMosher 的回應。
function moveFile(fileId, destinationFolderId) {
let destinationFolder = DriveApp.getFolderById(destinationFolderId);
DriveApp.getFileById(fileId).moveTo(destinationFolder);
}
所以原本的表單,使用上就變成了——
function createNewForm(){
let new_form = FormApp.create('New Form');
let new_form_id = new_form.getId()
let destinationFolderId = "your_folder_id_here"
moveFile(new_form_id, destinationFolderId)
}
那這邊是簡易生成一張表單的方式,接著我們要對每一張表單進行細節的操作,開始囉!
FormApp
來生出表單的問題我們要如何在 GAS 內生出問題們?我做了一個簡單方法對照表。
那實際上怎麼用呢,這邊先給大家看完整的程式碼,接著一個個說明。我們以一份要約 onsite interview 的表單為例。
function addNameText(form){
let text_name = form.addTextItem().setTitle('Name').setRequired(true);
return text_name
}
function addLunchList(form){
let list_item = form.addListItem();
list_item.setTitle('Option for the Lunch')
.setChoices([
list_item.createChoice('Meat'),
list_item.createChoice('Vegetarian')
]);
return list_item
}
function addTrafficChoices(form){
let multipleChoice_item = form.addMultipleChoiceItem();
multipleChoice_item.setTitle('How do you come to our office?')
.setChoices([
multipleChoice_item.createChoice('Train'),
multipleChoice_item.createChoice('Bus'),
multipleChoice_item.createChoice('Drive'),
multipleChoice_item.createChoice('Walk')
])
.showOtherOption(true);
return multipleChoice_item
}
function addInterestGridWithValidation(form){
let grid_item = form.addGridItem();
grid_item.setTitle('Rate your interests')
.setRows(['SDE', 'Test Engineer', 'Project Manager'])
.setColumns([5, 4, 3,2,1])
.setHelpText("It won't affect your scores in interviewing.");
let gridValidation = FormApp.createGridValidation()
.setHelpText("Select one item per column.")
.requireLimitOneResponsePerColumn()
.build();
grid_item.setValidation(gridValidation);
return grid_item
}
function addSkillCheckbox(form){
let checkbox_item = form.addCheckboxItem();
checkbox_item.setTitle('What are your technical skillsets')
.setChoices([
checkbox_item.createChoice('Python'),
checkbox_item.createChoice('JavaScript'),
checkbox_item.createChoice('HTML5'),
checkbox_item.createChoice('CSS3')
])
.showOtherOption(true);
return checkbox_item
}
function addAvailableDateTime(form){
let date_time_item = form.addDateTimeItem();
date_time_item.setTitle('When is your availability?');
return date_time_item
}
function addSuggestionParagraphText(form){
let paragraph_text_item = form.addParagraphTextItem();
paragraph_text_item.setTitle('Any question or suggestion?');
return paragraph_text_item
}
function addRateScale(form){
let scale_item = form.addScaleItem();
scale_item.setTitle('Rate this form')
.setBounds(1, 5);
return scale_item
}
function writeForm(curr_form){
curr_form.setTitle('D12 Form').setDescription('Description of form \nTest for new line');
let text_name = addNameText(curr_form);
let list_item = addLunchList(curr_form);
let multipleChoice_item = addTrafficChoices(curr_form);
let grid_item = addInterestGridWithValidation(curr_form);
let checkbox_item = addSkillCheckbox(curr_form);
let date_time_item = addAvailableDateTime(curr_form);
let paragraph_text_item = addSuggestionParagraphText(curr_form);
let scale_item = addRateScale(curr_form);
return curr_form
}
好,那我們一個個來講。順序依照最上面圖的順序。
這邊應該算好理解,針對 form
本身用 setTitle()
設定標題,也透過 setDescription()
設定標題下的敘述。
form.setTitle('D12 Form').setDescription('Description of form \n Test for new line');
眼尖的朋友應該有看到我有加入一個 '\n'
在 Description,這是「換行符號」,也就是在敘述段落如 description
時,可以透過加上這符號進行換行。換句話說,如果輸入
// 會出現連續的 123
.setDescription('123')
123
// 會出現分成三行的 1, 2, 3
.setDescription('1\n2\n3')
1
2
3
補充的是,這邊有三個功能是可以加上的,分別是收到回應的確認訊息、是否允許編輯與是否接受重複回應。
form.setConfirmationMessage('Thanks for responding!')
.setAllowResponseEdits(true)
.setAcceptingResponses(false);
對應的回應關係圖如下——
這張表是用中文版 Google Form 在新增問題時的順序,也是我們接下來列點介紹的順序。
這邊很簡單地用了 setTextItem()
作為了設定問題的方式,並且針對這個新增的問題用 setTitle()
來給予敘述,並且用 setRequired
來設定必填。
let text_name = form.addTextItem().setTitle('Name').setRequired(true);
而對應的段落也是用 .addParagraphTextItem()
即可。
let paragraph_text_item = form.addParagraphTextItem();
paragraph_text_item.setTitle('Any question or suggestion?');
但如果有時候我們想要加上一些限制,像是至少輸入 100 字,那要怎麼做?這時就要用到 createParagraphTextValidation
來執行。範例程式碼如下——
let paragraphtextValidation = FormApp.createParagraphTextValidation()
.setHelpText(“Answer must be more than 100 characters.”)
.requireTextLengthGreatherThan(100);
paragraph_text_item.setValidation(paragraphtextValidation);
而其總共有六種模式可以設定,分別是...
需要含有
以下 Pattern requireTextContainsPattern(pattern)
:通常是開頭、結尾需要是特定格式。 e.g. email 的信箱位址。不得含有
以下 Pattern requireTextDoesNotContainPattern(pattern)
需要吻合
以下 Pattern requireTextMatchesPattern(pattern)
:通常是字句中需要含有特定關鍵字、模式。 e.g. 含有三碼郵遞區號數字後接上文字不得吻合
以下 Pattern requireTextDoesNotMatchPattern(pattern)
長度大於或等於
數字requireTextLengthGreaterThanOrEqualTo(number)
:e.g. 地址文字中不得含有數字(需用國字中文之類)長度小於或等於
數字 requireTextLengthLessThanOrEqualTo(number)
上面的功能中所提到的 Pattern
,其實就是 Regex(Regular Expression 正規表示式)。這邊給一個使用的範例。
let paragraphtextValidation = FormApp.createParagraphTextValidation()
.requireTextContainsPattern('[a-zA-Z]')
paragraph_text_item.setValidation(paragraphtextValidation);
上面所寫的這個範例就是,檢查輸入的內容只能是英文大寫 [A-Z]
或小寫 [a-z]
。更詳細 Regex 可以到 regexone 和 learn regex 學,很詳盡。
下面這段程式碼,主要是先用 addMultipleChoiceItem()
創造一個 object,並接著用 setChoices()
和 createChoice)_
來創造選項們,最後設定 showOtherOption()
來讓填答人可以自行輸入「其他」。
let multipleChoice_item = form.addMultipleChoiceItem();
multipleChoice_item.setTitle('How do you come to our office?')
.setChoices([
multipleChoice_item.createChoice('Train'),
multipleChoice_item.createChoice('Bus'),
multipleChoice_item.createChoice('Drive'),
multipleChoice_item.createChoice('Walk')
])
.showOtherOption(true);
方式跟設定選擇題幾乎一樣,只差在是用 addCheckboxItem()
來建造。
let checkbox_item = form.addCheckboxItem();
checkbox_item.setTitle('What are your technical skillsets')
.setChoices([
checkbox_item.createChoice('Python'),
checkbox_item.createChoice('JavaScript'),
checkbox_item.createChoice('HTML5'),
checkbox_item.createChoice('CSS3')
])
.showOtherOption(true);
額外有 CheckboxValidationBuilder
可以建造驗證程序,方式包括
requireSelectAtLeast()
requireSelectAtMost()
requireSelectExactly()
提供官方範例給大家參考~
var checkBoxValidation = FormApp.createCheckboxValidation()
.setHelpText(“Select two condiments.”)
.requireSelectExactly(2)
.build();
checkBoxItem.setValidation(checkBoxValidation);
透過 addListItem()
來建置即可,比較沒看到特別好玩的部分
let list_item = form.addListItem();
list_item.setTitle('Option for the Lunch')
.setChoices([
list_item.createChoice('Meat'),
list_item.createChoice('Vegetarian')
]);
透過 addScaleItem()
來建置。基本上是輸入數值。那身為中文使用者,會很想問說,那怎麼樣輸入中文?這時就要透過 setLabels('Bad', 'Good')
的方式。
let scale_item = form.addScaleItem();
scale_item.setTitle('Rate this form')
.setBounds(1, 5)
.setBounds('Bad', 'Good');
藉由 addGridItem
來新增。
let grid_item = form.addGridItem();
grid_item.setTitle('Rate your interests')
.setRows(['SDE', 'Test Engineer', 'Project Manager'])
.setColumns([5, 4, 3,2,1])
.setHelpText("It won't affect your scores in interviewing.");
且一樣有 GridValidationBuilder 可以建立。但方式只有一種,也就是限制每一直欄都只能有一個被填入:requireLimitOneResponsePerColumn()
。程式碼如下。
let gridValidation = FormApp.createGridValidation()
.setHelpText("Select one item per column.")
.requireLimitOneResponsePerColumn()
.build();
grid_item.setValidation(gridValidation);
那會想問,如果我想設定的是每個橫的行,都必須要填入一個選項呢?就單純用 .setRequired(true)
即可做到了。
那至於「核取方塊格」呢?因為方式都一樣,所以可以單純地把 addGridItem
換成 addCheckboxGridItem
。兩者的 validation method 也都都是只有一種,但核取方塊格需要將 GridValidationBuilder 換成 CheckboxGridValidationBuilder()
就是。
日期與時間也相對單純,用 addDateTimeItem()
即可。
let date_time_item = form.addDateTimeItem();
date_time_item.setTitle('When is your availability?');
好,但我們的重點是客製化表單,要怎麼樣將原本的做表格變客製化呢?這邊先用個簡單的方式。
function writeForm(){
let data = readData();
let new_forms_id_arr = [];
for(row_data of data){
let form_name = row_data[0];
let form_description = row_data[1];
let curr_form = FormApp.create(form_name);
let curr_form_id = curr_form.getId()
new_forms_id_arr.push([curr_form_id]);
moveFile(curr_form_id, target_folder_ID)
curr_form.setTitle(form_name).setDescription(form_description);
let question_function_list = [addNameText,
addLunchList,
addTrafficChoices,
addInterestGridWithValidation,
addSkillCheckbox,
addAvailableDateTime,
addSuggestionParagraphText,
]
for(let i = 2; i< row_data.length; i++){
if(row_data[i] == true){
question_function_list[i-2](curr_form);
}
}
// add general item
addRateScale(curr_form);
}
writeData(new_forms_id_arr)
}
裡頭要用到的功能上方都有寫,可以直接複製喔!
其實已經偷偷寫在上面的 Code 裡面了,請看 Step 3 最後 writeData()
的部分。
完整執行畫面——
回對我們的任務表,確認 Ben 表單中是沒有「下拉選單」午餐編號的。
任務完成!
好,我們總算完成了(落淚),雖然昨日 D11 介紹的第一種方式比較簡單,但實際上想彈性運用,我們會需要很多 D12 的內容。但,如果我們今天真的生了 100 份表單,那要怎麼樣統一回應?總不能慢慢搜集吧。這時候就會需要看我們的 D13 了。
不知不覺就寫了一整天...Orz,希望大家喜歡。一樣提醒,用FormApp.create()
來創造表單是有 Quota 限制——每天不超過 250 份。如果還有問題,透過留言之外,也可以到 Facebook Group,想開很久這次鐵人賽才真的開起來哈哈哈,歡迎來當 Founding Member。如果不想錯過可以訂閱按讚小鈴鐺(?),也歡迎留言跟我說你還想知道什麼做法/主題。我們明天見。